defmodule LibPayment.WechatPay do
  @moduledoc false

  import LibPayment.Util

  alias LibPayment.{Const}

  require Logger

  @type method :: :get | :post | :put | :patch | :delete | :options | :head
  @type err_t :: {:err, term()} | :error

  @tag_length 16
  @service_host "https://api.mch.weixin.qq.com"

  @type pem :: binary()
  @type serial_no :: String.t()
  @type name :: pid | atom | String.t()

  @type payment_cfg :: [
          name: name(),
          appid: String.t(),
          mchid: String.t(),
          notify_url: String.t(),
          apiv3_key: binary(),
          # 微信平台证书列表
          wx_pubs: [{serial_no(), pem()}],
          # 商户证书序列号
          client_serial_no: serial_no(),
          # 商户私钥
          client_key: pem(),
          # 商户证书
          client_cert: pem()
        ]
  @type cli :: [
          appid: String.t(),
          mchid: String.t(),
          notify_url: String.t(),
          apiv3_key: binary(),
          # 微信平台证书列表
          wx_pubs: [{serial_no(), pem()}],
          # 商户证书序列号
          client_serial_no: serial_no(),
          # 商户私钥
          client_key: term(),
          # 商户证书
          client_cert: term()
        ]

  @user_agents [
    "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
    "Mozilla/5.0 (Linux; U; Android 2.2.1; zh-cn; HTC_Wildfire_A3333 Build/FRG83D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
    "Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"
  ]

  def child_spec(opts) do
    %{
      id: __MODULE__,
      type: :worker,
      start: {__MODULE__, :start_link, [opts]}
    }
  end

  def start_link(opts) do
    Const.put({__MODULE__, opts[:name]}, %{
      appid: opts[:appid],
      mchid: opts[:mchid],
      notify_url: opts[:notify_url],
      apiv3_key: opts[:apiv3_key],
      wx_pubs: opts[:wx_pubs],
      client_serial_no: opts[:client_serial_no],
      client_key: load_pem(opts[:client_key]),
      client_cert: load_pem(opts[:client_cert])
    })

    Agent.start_link(fn -> %{} end)
  end

  defp get_client(name), do: Const.get({__MODULE__, name})

  @spec decrypt_response(name(), map()) :: {:ok, binary()} | err_t()
  def decrypt_response(name, data) do
    cli = get_client(name)
    decrypt_data(cli[:apiv3_key], data)
  end

  @doc """
  获取平台证书
  后续用 openssl x509 -in some_cert.pem -pubkey 导出平台公钥

  iex> get_certificates(:demo)
  {
    :ok,
    [
      %{
        "cert" => "-----BEGIN CERTIFICATE-----xxx-----END CERTIFICATE-----",
        "effective_time" => "2021-06-23T14:09:22+08:00",
        "expire_time" => "2026-06-22T14:09:22+08:00",
        "serial_no" => "35CE31ED8F4A50B930FF8D37C51B5ADA03265E7X"
      }
    ]
  }
  """
  @spec get_certificates(name()) :: {:ok, [map()]} | err_t()
  def get_certificates(name) do
    with cli <- get_client(name),
         ua <- Enum.random(@user_agents),
         {:ok, %{"data" => data}} <-
           request(cli, "/v3/certificates", :get, %{}, %{}, [{"User-Agent", ua}]),
         res <-
           Enum.map(
             data,
             &%{
               "effective_time" => &1["effective_time"],
               "expire_time" => &1["expire_time"],
               "serial_no" => &1["serial_no"],
               "cert" => decrypt_data!(cli[:apiv3_key], &1["encrypt_certificate"])
             }
           ) do
      {:ok, res}
    end
  end

  @spec verify_response(name, %{String.t() => any()}, binary) :: boolean() | err_t()
  def verify_response(name, headers, body) do
    with cli <- get_client(name), do: verify(cli[:wx_pubs], headers, body)
  end

  @doc """
  Doc: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml

  ## Examples

  iex> create_jsapi_tranaction(
      :demo,
      %{
        "out_trade_no" => "order_xxxx",
        "description" => "测试订单",
        "amount" => %{"total" => 1},
        "payer" => %{"openid" => "ohNY75Jw8MlsKuu4cFBbjmK4ZPxxx"}
      },
      recv_timeout: 2000)
  {:ok, %{"prepay_id" => "wx25104640294460668d258585313de91000"}}
  """
  @spec create_jsapi_transaction(name, map(), keyword()) :: {:ok, term()} | err_t()
  def create_jsapi_transaction(name, data, opts \\ [recv_timeout: 5000]) do
    with cli <- get_client(name),
         data <-
           Map.merge(data, %{
             "appid" => cli[:appid],
             "mchid" => cli[:mchid],
             "notify_url" => cli[:notify_url]
           }) do
      request(cli, "/v3/pay/transactions/jsapi", :post, %{}, data, [], opts)
    end
  end

  @doc """

  Doc: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml

  ## Examples
  iex> create_native_transaction(
      :demo,
      %{
        "out_trade_no" => "order_1100",
        "description" => "测试订单",
        "amount" => %{"total" => 1}
      },
      recv_timeout: 2000
  )
  {:ok, %{"code_url" => "weixin://wxpay/bizpayurl?pr=A9ceSdqzk"}}
  """
  @spec create_native_transaction(name, map(), keyword()) :: {:ok, map()} | err_t()
  def create_native_transaction(name, data, opts \\ [recv_timeout: 5000]) do
    with cli <- get_client(name),
         data <-
           Map.merge(data, %{
             "appid" => cli[:appid],
             "mchid" => cli[:mchid],
             "notify_url" => cli[:notify_url]
           }) do
      request(cli, "/v3/pay/transactions/native", :post, %{}, data, [], opts)
    end
  end

  @doc """
  Doc: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml

  iex> create_h5_transaction(:demo, %{
          "out_trade_no" => "order_1102",
          "description" => "测试订单",
          "amount" => %{"total" => 1},
          "scene_info" => %{"payer_client_ip" => "127.0.0.1"}
        },
        recv_timeout: 2000
  )
  {:ok, %{"ht_url" => "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx2916263004719461949c84457c735b0000&package=2150917749"}}
  """
  @spec create_h5_transaction(name(), %{String.t() => any()}, keyword()) :: {:ok, map()} | err_t()
  def create_h5_transaction(name, data, opts \\ [recv_timeout: 5000]) do
    with cli <- get_client(name),
         data <-
           Map.merge(data, %{
             "appid" => cli[:appid],
             "mchid" => cli[:mchid],
             "notify_url" => cli[:notify_url]
           }) do
      request(cli, "/v3/pay/transactions/h5", :post, %{}, data, [], opts)
    end
  end

  @doc """
  Doc: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml

  ## Examples
  iex> query_transaction(:demo, "T64228334BNA1G3")
  {:ok,
  %{
    "amount" => %{
      "currency" => "CNY",
      "payer_currency" => "CNY",
      "payer_total" => 10,
      "total" => 10
    },
    "appid" => "wx6296faa4dbb0ab3b",
    "attach" => "",
    "bank_type" => "OTHERS",
    "mchid" => "1604124248",
    "out_trade_no" => "T64228334BNA1G3",
    "payer" => %{"openid" => "oxUt85U6N0G-7shznSjdqCnp03-Y"},
    "promotion_detail" => [],
    "success_time" => "2022-05-06T11:58:14+08:00",
    "trade_state" => "SUCCESS",
    "trade_state_desc" => "支付成功",
    "trade_type" => "JSAPI",
    "transaction_id" => "4200001430202205065075126954"
  }}
  """
  @spec query_transaction(name :: name(), trade_no :: String.t(), opts :: keyword()) ::
          {:ok, map()} | err_t()
  def query_transaction(name, trade_no, opts \\ [recv_timeout: 5000]) do
    cli = get_client(name)
    api = "/v3/pay/transactions/out-trade-no/#{trade_no}"
    request(cli, api, :get, %{"mchid" => cli[:mchid]}, %{}, [], opts)
  end

  @doc """
  Doc: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml

  ## Examples

  iex> create_refund(
    :demo,
    %{
      "out_trade_no" => "order_xxxx",
      "out_refund_no" => "refund_xxxx",
      "reason" => "退款",
      "amount" => %{
        "refund" => 1,
        "total" => 1,
        "currency" => "CNY"
      }
    },
    recv_timeout: 2000
  )
  {:ok, return_value}
  """
  @spec create_refund(name(), map(), keyword()) :: {:ok, map()} | err_t()
  def create_refund(name, data, opts \\ [recv_timeout: 5000]) do
    with cli <- get_client(name) do
      request(cli, "/v3/refund/domestic/refunds", :post, %{}, data, [], opts)
    end
  end

  @doc """
  生成小程序支付表单
  function description

  ## Examples

  iex> miniapp_payform(:demo, "wx28094533993528b1d687203f4f48e20000")
  %{
    "appid" => "wxefd6b215fca0cacd",
    "nonceStr" => "ODnHX8RwAlw0",
    "package" => "prepay_id=wx28094533993528b1d687203f4f48e20000",
    "paySign" => "xxxx",
    "signType" => "RSA",
    "timeStamp" => 1624844734
  }
  """
  @spec miniapp_payform(name(), String.t()) :: map()
  def miniapp_payform(name, prepay_id) do
    with cli <- get_client(name),
         ts <- timestamp(),
         nonce <- random_string(12),
         package <- "prepay_id=#{prepay_id}",
         sign <- sign_miniapp(cli[:appid], ts, nonce, package, cli[:client_key]) do
      %{
        "appid" => cli[:appid],
        "timeStamp" => ts,
        "nonceStr" => nonce,
        "package" => package,
        "signType" => "RSA",
        "paySign" => sign
      }
    end
  end

  @spec request(
          cli :: cli(),
          api :: String.t(),
          method :: method(),
          query :: map() | keyword(),
          data :: map() | nil,
          headers :: list(),
          opts :: list()
        ) ::
          {:ok, any()} | err_t()
  def request(cli, api, method, query, data, headers \\ [], opts \\ [recv_timeout: 5000])

  def request(cli, api, method, query, data, headers, opts) do
    with ts <- timestamp(),
         nonce_str <- random_string(12),
         signature <- sign(method, api, query, data, nonce_str, ts, cli[:client_key]),
         auth <-
           "mchid=\"#{cli[:mchid]}\",nonce_str=\"#{nonce_str}\",timestamp=\"#{ts}\",serial_no=\"#{cli[:client_serial_no]}\",signature=\"#{signature}\"",
         full_headers <- [
           {"Content-Type", "application/json"},
           {"Accept", "application/json"},
           {"Authorization", "WECHATPAY2-SHA256-RSA2048 " <> auth}
           | headers
         ],
         req <- %HTTPoison.Request{
           method: method,
           url: gen_uri(api),
           headers: full_headers,
           body: Jason.encode!(data),
           params: query,
           options: opts
         },
         {:ok, %HTTPoison.Response{body: body, status_code: 200, headers: headers}} <-
           HTTPoison.request(req),
         headers <- Enum.into(headers, %{}, fn {k, v} -> {String.downcase(k), v} end),
         true <- verify(cli[:wx_pubs], headers, body) do
      Jason.decode(body)
    else
      {:ok, %HTTPoison.Response{body: body}} ->
        {:error, body}

      {:error, msg} ->
        {:error, msg}

      other_error ->
        Logger.error(%{"api" => api, "error" => inspect(other_error)})
        {:error, "Interal server error"}
    end
  end

  @spec sign(
          :get | :post,
          String.t(),
          %{String.t() => any()},
          %{String.t() => any()},
          String.t(),
          integer,
          term()
        ) :: binary
  defp sign(method, api, query, data, nonce_str, timestamp, client_key) do
    {http_method, body} =
      case method do
        :post -> {"POST", Jason.encode!(data)}
        :get -> {"GET", ""}
      end

    query_str =
      if query in [%{}, nil] do
        ""
      else
        "?" <> URI.encode_query(query)
      end

    string_to_sign = "#{http_method}\n#{api}#{query_str}\n#{timestamp}\n#{nonce_str}\n#{body}\n"

    Logger.debug(%{"string2sign" => "#{string_to_sign}"})

    string_to_sign
    |> :public_key.sign(:sha256, client_key)
    |> Base.encode64()
  end

  @spec verify([{serial_no(), pem()}], %{String.t() => String.t()}, binary()) ::
          boolean() | {:error, String.t()}
  def verify(wx_pubs, headers, body) do
    with {_, wx_pub} <-
           Enum.find(wx_pubs, fn {x, _} -> x == headers["wechatpay-serial"] end),
         wx_pub_key <- load_pem(wx_pub),
         ts <- headers["wechatpay-timestamp"],
         nonce <- headers["wechatpay-nonce"],
         string_to_sign <- "#{ts}\n#{nonce}\n#{body}\n",
         encoded_wx_signature <- headers["wechatpay-signature"],
         {:ok, wx_signature} <- Base.decode64(encoded_wx_signature) do
      :public_key.verify(string_to_sign, :sha256, wx_signature, wx_pub_key)
    else
      reason ->
        Logger.error(%{"body" => body, "error" => inspect(reason)})
        {:error, "wechat response verify error"}
    end
  end

  @spec sign_miniapp(String.t(), integer, String.t(), String.t(), term()) :: binary
  def sign_miniapp(appid, ts, nonce, package, client_key) do
    string_to_sign = "#{appid}\n#{ts}\n#{nonce}\n#{package}\n"

    Logger.debug(%{"string2sign" => "#{string_to_sign}"})

    string_to_sign
    |> :public_key.sign(:sha256, client_key)
    |> Base.encode64()
  end

  @spec decrypt_data(term(), %{String.t() => any()}) :: {:ok, binary} | {:error, any}
  def decrypt_data(apiv3_key, %{
        "algorithm" => "AEAD_AES_256_GCM",
        "associated_data" => aad,
        "ciphertext" => encoded_ciphertext,
        "nonce" => nonce
      }) do
    with {:ok, ciphertext} <- Base.decode64(encoded_ciphertext),
         size_total <- byte_size(ciphertext),
         ctext_len <- size_total - @tag_length,
         <<ctext::binary-size(ctext_len), tag::binary-size(@tag_length)>> <- ciphertext,
         ret <-
           :crypto.crypto_one_time_aead(
             :aes_256_gcm,
             apiv3_key,
             nonce,
             ctext,
             aad,
             tag,
             false
           ) do
      {:ok, ret}
    end
  end

  def decrypt_data(_, _data), do: {:error, "invalida data form"}

  def decrypt_data!(key, map) do
    decrypt_data(key, map)
    |> case do
      {:ok, ret} -> ret
      err -> err
    end
  end

  defp gen_uri(api) do
    @service_host
    |> URI.merge(api)
    |> to_string()
  end
end
